/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package io.milton.http.json;

import io.milton.resource.ReplaceableResource;
import io.milton.common.FileUtils;
import io.milton.common.Utils;
import io.milton.event.EventManager;
import io.milton.event.PutEvent;
import io.milton.http.*;
import io.milton.http.Request.Method;
import io.milton.http.exceptions.BadRequestException;
import io.milton.http.exceptions.ConflictException;
import io.milton.http.exceptions.NotAuthorizedException;
import io.milton.resource.DeletableResource;
import io.milton.resource.PostableResource;
import io.milton.resource.PutableResource;
import io.milton.resource.Resource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sf.json.JSON;
import net.sf.json.JSONSerializer;
import net.sf.json.JsonConfig;
import net.sf.json.util.CycleDetectionStrategy;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Will use milton's PUT framework to support file uploads using POST and
 * multipart encoding
 * <p>
 * This will save the uploaded files with their given names into the parent
 * collection resource.
 * <p>
 * If a file already exists with the same name a ConflictException is thrown,
 * unless you set the _autoname request parameter. If this parameter is present
 * (ie with any value) the file will be saved with a non-conflicting file name
 * <p>
 * Save file information is returned as JSON in the response content
 *
 * @author brad
 */
public class PutJsonResource extends JsonResource implements PostableResource {

    private static final Logger log = LoggerFactory.getLogger(PutJsonResource.class);
    public static final String PARAM_AUTONAME = "_autoname";
    public static final String PARAM_NAME = "name";
    public static final String PARAM_OVERWRITE = "overwrite";
    private final EventManager eventManager;
    private final PutableResource wrapped;
    private final String href;
    private List<NewFile> newFiles;
    private String errorMessage;

    public PutJsonResource(PutableResource putableResource, String href, EventManager eventManager) {
        super(putableResource, Request.Method.PUT.code, null);
        this.eventManager = eventManager;
        this.wrapped = putableResource;
        this.href = href;
    }

    @Override
    public String getContentType(String accepts) {
        //String s = "application/x-javascript; charset=utf-8";
        return "text/plain";
        //return "application/json";
    }

    @Override
    public String processForm(Map<String, String> parameters, Map<String, FileItem> files) throws ConflictException, NotAuthorizedException, BadRequestException {
        log.info("processForm: " + wrapped.getClass());

        newFiles = new ArrayList<>();

        try {
            if (parameters.containsKey("content")) {
                String name = parameters.get("name");
                String content = parameters.get("content");
                String contentType = parameters.get("Content-Type");
                byte[] arr = toArray(content);
                ByteArrayInputStream in = new ByteArrayInputStream(arr);
                long length = arr.length;
                NewFile nf = new NewFile();
                nf.setOriginalName(name);
                nf.setContentType(contentType);
                nf.setLength(length);
                newFiles.add(nf);

                processFile(name, in, length, contentType, nf);
            }

            for (FileItem file : files.values()) {
                NewFile nf = new NewFile();
                String ua = HttpManager.request().getUserAgentHeader();
                String f = Utils.truncateFileName(ua, file.getName());
                nf.setOriginalName(f);
                nf.setContentType(file.getContentType());
                nf.setLength(file.getSize());
                newFiles.add(nf);
                String newName = getName(f, parameters);

                log.info("creating resource: " + newName + " size: " + file.getSize());
                InputStream in = null;
                try {
                    in = file.getInputStream();
                    processFile(newName, in, file.getSize(), file.getContentType(), nf);
                } finally {
                    FileUtils.close(in);
                }
            }
            log.trace("completed all POST processing");
        } catch (BadRequestException e) {
            log.warn("BadRequestException", e);
            errorMessage = "Bad Request: " + e.getReason();
        } catch (NotAuthorizedException e) {
            log.warn("NotAuthorizedException", e);
            errorMessage = "Not authorised: " + e.getMessage();
        } catch (ConflictException e) {
            log.warn("ConflictException", e);
            errorMessage = "Conflict: " + e.getMessage();
        }
        return null;
    }

    private byte[] toArray(String s) {
        try {
            return s.getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private void processFile(String newName, InputStream in, Long length, String contentType, NewFile nf) throws BadRequestException, NotAuthorizedException, ConflictException {
        Resource newResource;

        try {
            Resource existing = wrapped.child(newName);
            if (existing != null) {
                if (existing instanceof ReplaceableResource) {
                    log.trace("existing resource is replaceable, so replace content");
                    ReplaceableResource rr = (ReplaceableResource) existing;
                    rr.replaceContent(in, null);
                    log.trace("completed POST processing for file. Updated: " + existing.getName());
                    eventManager.fireEvent(new PutEvent(rr));
                    newResource = rr;
                } else {
                    log.trace("existing resource is not replaceable, will be deleted");
                    if (existing instanceof DeletableResource) {
                        DeletableResource dr = (DeletableResource) existing;
                        dr.delete();
                        newResource = wrapped.createNew(newName, in, length, contentType);
                        log.trace("completed POST processing for file. Deleted, then created: " + newResource.getName());
                        eventManager.fireEvent(new PutEvent(newResource));
                    } else {
                        throw new BadRequestException(existing, "existing resource could not be deleted, is not deletable");
                    }
                }
            } else {
                newResource = wrapped.createNew(newName, in, length, contentType);
                if (newResource != null) {
                    log.info("completed POST processing for file. Created: " + newResource.getName());
                } else {
                    log.info("completed POST processing for file. null resource returned");
                }
                eventManager.fireEvent(new PutEvent(newResource));
            }
            String newHref = buildNewHref(href, newResource.getName());
            nf.setHref(newHref);
        } catch (IOException ex) {
            throw new RuntimeException("Exception creating resource", ex);
        } finally {
        }
    }

    /**
     * Returns a JSON representation of the newly created hrefs
     *
     * @param out
     * @param range
     * @param params
     * @param contentType
     * @throws IOException
     * @throws NotAuthorizedException
     */
    @Override
    public void sendContent(OutputStream out, Range range, Map<String, String> params, String contentType) throws IOException, NotAuthorizedException {
        JsonConfig cfg = new JsonConfig();
        cfg.setIgnoreTransientFields(true);
        cfg.setCycleDetectionStrategy(CycleDetectionStrategy.LENIENT);
        Writer writer = new PrintWriter(out);
        if (errorMessage != null) {
            Map map = new HashMap();
            map.put("error", errorMessage);
            JSON json = JSONSerializer.toJSON(map, cfg);
            json.write(writer);

        } else {
            NewFile[] arr;
            if (newFiles != null) {
                arr = new NewFile[newFiles.size()];
                newFiles.toArray(arr);
            } else {
                arr = new NewFile[0];
            }
            JSON json = JSONSerializer.toJSON(arr, cfg);
            json.write(writer);
        }
        writer.flush();
    }

    @Override
    public Method applicableMethod() {
        return Method.PUT;
    }

    private String getName(String filename, Map<String, String> parameters) throws ConflictException, NotAuthorizedException, BadRequestException {
        String initialName = filename;
        if (parameters.containsKey(PARAM_NAME)) {
            initialName = parameters.get(PARAM_NAME);
        }
        boolean nonBlankName = initialName != null && initialName.trim().length() > 0;
        boolean autoname = (parameters.get(PARAM_AUTONAME) != null);
        boolean overwrite = (parameters.get(PARAM_OVERWRITE) != null);
        if (nonBlankName) {
            Resource child = wrapped.child(initialName);
            if (child == null) {
                log.trace("no existing file with that name");
                return initialName;
            } else {
                if (overwrite) {
                    log.trace("file exists, and overwrite parameters is set, so allow overwrite: " + initialName);
                    return initialName;
                } else {
                    if (!autoname) {
                        log.warn("Conflict: Can't create resource with name " + initialName + " because it already exists. To rename automatically use request parameter: " + PARAM_AUTONAME + ", or to overwrite use " + PARAM_OVERWRITE);
                        throw new ConflictException(this);
                    } else {
                        log.trace("file exists and autoname is set, so will find acceptable name");
                    }
                }
            }
        } else {
            initialName = getDateAsName("upload");
            log.trace("no name given in request");
        }
        return findAcceptableName(initialName);
    }

    private String getDateAsName(String base) {
        Calendar cal = Calendar.getInstance();
        return base + "_" + cal.get(Calendar.YEAR) + "-" + cal.get(Calendar.MONTH) + "-" + cal.get(Calendar.DAY_OF_MONTH);
    }

    private String findAcceptableName(String initialName) throws ConflictException, NotAuthorizedException, BadRequestException {
        String baseName = FileUtils.stripExtension(initialName);
        String ext = FileUtils.getExtension(initialName);
        return findAcceptableName(baseName, ext, 1);
    }

    private String findAcceptableName(String baseName, String ext, int i) throws ConflictException, NotAuthorizedException, BadRequestException {
        String candidateName = baseName + "_" + i;
        if (ext != null && ext.length() > 0) {
            candidateName += "." + ext;
        }
        if (wrapped.child(candidateName) == null) {
            return candidateName;
        } else {
            if (i < 100) {
                return findAcceptableName(baseName, ext, i + 1);
            } else {
                log.warn("Too many files with similar names: " + candidateName);
                throw new ConflictException(this);
            }
        }
    }

    private String buildNewHref(String href, String newName) {
        String s = href;
        int pos = href.lastIndexOf("_DAV");
        s = s.substring(0, pos - 1);
        if (!s.endsWith("/")) {
            s += "/";
        }
        s += newName;
        return s;
    }

    public static class NewFile {

        private String href;
        private String originalName;
        private long length;
        private String contentType;

        public String getHref() {
            return href;
        }

        public void setHref(String href) {
            this.href = href;
        }

        public String getOriginalName() {
            return originalName;
        }

        public void setOriginalName(String originalName) {
            this.originalName = originalName;
        }

        public long getLength() {
            return length;
        }

        public void setLength(long length) {
            this.length = length;
        }

        public String getContentType() {
            return contentType;
        }

        public void setContentType(String contentType) {
            this.contentType = contentType;
        }
    }
}
